/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flowable.engine.impl.webservice;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;

import javax.wsdl.Definition;
import javax.wsdl.Types;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.schema.Schema;
import javax.xml.namespace.QName;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.common.i18n.UncheckedException;
import org.apache.cxf.endpoint.dynamic.DynamicClientFactory;
import org.apache.cxf.resource.URIResolver;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.OperationInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.wsdl.WSDLManager;
import org.apache.cxf.wsdl11.WSDLServiceBuilder;
import org.flowable.bpmn.model.Import;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.impl.util.ReflectUtil;
import org.flowable.engine.impl.bpmn.data.PrimitiveStructureDefinition;
import org.flowable.engine.impl.bpmn.data.SimpleStructureDefinition;
import org.flowable.engine.impl.bpmn.data.StructureDefinition;
import org.flowable.engine.impl.bpmn.parser.XMLImporter;

import com.ibm.wsdl.extensions.schema.SchemaImpl;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JJavaName;
import com.sun.codemodel.JType;
import com.sun.tools.xjc.ConsoleErrorReporter;
import com.sun.tools.xjc.api.ErrorListener;
import com.sun.tools.xjc.api.Mapping;
import com.sun.tools.xjc.api.S2JJAXBModel;
import com.sun.tools.xjc.api.SchemaCompiler;
import com.sun.tools.xjc.api.XJC;

/**
 * @author Esteban Robles Luna
 */
public class CxfWSDLImporter implements XMLImporter {

    protected static final String JAXB_BINDINGS_RESOURCE = "flowable-bindings.xjc";

    protected Map<String, WSService> wsServices = new HashMap<>();
    protected Map<String, WSOperation> wsOperations = new HashMap<>();
    protected Map<String, StructureDefinition> structures = new HashMap<>();

    protected String wsdlLocation;
    protected String namespace;

    public CxfWSDLImporter() {
        this.namespace = "";
    }

    @Override
    public void importFrom(Import theImport, String sourceSystemId) {
        this.namespace = theImport.getNamespace() == null ? "" : theImport.getNamespace() + ":";
        try {
            final URIResolver uriResolver = this.createUriResolver(sourceSystemId, theImport);
            if (uriResolver.isResolved()) {
                if (uriResolver.getURI() != null) {
                    this.importFrom(uriResolver.getURI().toString());
                } else if (uriResolver.isFile()) {
                    this.importFrom(uriResolver.getFile().getAbsolutePath());
                } else if (uriResolver.getURL() != null) {
                    this.importFrom(uriResolver.getURL().toString());
                }
            } else {
                throw new UncheckedException(new Exception("Unresolved import against " + sourceSystemId));
            }

        } catch (final IOException e) {
            throw new UncheckedException(e);
        }
    }

    protected URIResolver createUriResolver(String sourceSystemId, Import theImport) throws IOException {
        return new URIResolver(sourceSystemId, theImport.getLocation());
    }

    public void importFrom(String url) {
        this.wsServices.clear();
        this.wsOperations.clear();
        this.structures.clear();

        this.wsdlLocation = url;

        try {
            Bus bus = BusFactory.getDefaultBus();
            final Enumeration<URL> xjcBindingUrls = Thread.currentThread().getContextClassLoader().getResources(JAXB_BINDINGS_RESOURCE);
            if (xjcBindingUrls.hasMoreElements()) {
                final URL xjcBindingUrl = xjcBindingUrls.nextElement();
                if (xjcBindingUrls.hasMoreElements()) {
                    throw new FlowableException("Several JAXB binding definitions found for flowable-cxf: " + JAXB_BINDINGS_RESOURCE);
                }
                DynamicClientFactory.newInstance(bus).createClient(url, Arrays.asList(new String[] { xjcBindingUrl.toString() }));
                WSDLManager wsdlManager = bus.getExtension(WSDLManager.class);
                Definition def = wsdlManager.getDefinition(url);
                WSDLServiceBuilder builder = new WSDLServiceBuilder(bus);
                List<ServiceInfo> services = builder.buildServices(def);

                for (ServiceInfo service : services) {
                    WSService wsService = this.importService(service);
                    this.wsServices.put(this.namespace + wsService.getName(), wsService);
                }

                if (def != null && def.getTypes() != null) {
                    this.importTypes(def.getTypes());
                }
            } else {
                throw new FlowableException("The JAXB binding definitions are not found for flowable-cxf: " + JAXB_BINDINGS_RESOURCE);
            }
        } catch (WSDLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            throw new FlowableException("Error retrieving the JAXB binding definitions", e);
        }
    }

    protected WSService importService(ServiceInfo service) {
        String name = service.getName().getLocalPart();
        String location = "";

        for (EndpointInfo endpoint : service.getEndpoints()) {
            location = endpoint.getAddress();
        }

        WSService wsService = new WSService(this.namespace + name, location, this.wsdlLocation);
        for (OperationInfo operation : service.getInterface().getOperations()) {
            WSOperation wsOperation = this.importOperation(operation, wsService);
            wsService.addOperation(wsOperation);

            this.wsOperations.put(this.namespace + operation.getName().getLocalPart(), wsOperation);
        }
        return wsService;
    }

    protected WSOperation importOperation(OperationInfo operation, WSService service) {
        WSOperation wsOperation = new WSOperation(this.namespace + operation.getName().getLocalPart(), operation.getName().getLocalPart(), service);
        return wsOperation;
    }

    protected void importTypes(Types types) {
        SchemaCompiler compiler = XJC.createSchemaCompiler();
        ErrorListener elForRun = new ConsoleErrorReporter();
        compiler.setErrorListener(elForRun);

        SchemaImpl impl = (SchemaImpl) types.getExtensibilityElements().get(0);

        S2JJAXBModel intermediateModel = this.compileModel(types, compiler, impl.getElement());
        Collection<? extends Mapping> mappings = intermediateModel.getMappings();

        for (Mapping mapping : mappings) {
            this.importStructure(mapping);
        }
    }

    protected void importStructure(Mapping mapping) {
        QName qname = mapping.getElement();
        final JType type = mapping.getType().getTypeClass();
        if (type.isPrimitive()) {
            final Class<?> primitiveClass = ReflectUtil.loadClass(type.boxify().fullName());
            final StructureDefinition structure = new PrimitiveStructureDefinition(this.namespace + qname.getLocalPart(), primitiveClass);
            this.structures.put(structure.getId(), structure);

        } else if (type instanceof JDefinedClass) {
            JDefinedClass theClass = (JDefinedClass) type;
            SimpleStructureDefinition structure = new SimpleStructureDefinition(this.namespace + qname.getLocalPart());
            this.structures.put(structure.getId(), structure);

            importFields(theClass, structure);

        } else {
            final Class<?> referencedClass = ReflectUtil.loadClass(type.fullName());
            final StructureDefinition structure = new PrimitiveStructureDefinition(this.namespace + qname.getLocalPart(), referencedClass);
            this.structures.put(structure.getId(), structure);
        }
    }

    protected static void importFields(final JDefinedClass theClass, final SimpleStructureDefinition structure) {
        final AtomicInteger index = new AtomicInteger(0);
        _importFields(theClass, index, structure);
    }

    protected static void _importFields(final JDefinedClass theClass, final AtomicInteger index, final SimpleStructureDefinition structure) {

        final JClass parentClass = theClass._extends();
        if (parentClass instanceof JDefinedClass) {
            _importFields((JDefinedClass) parentClass, index, structure);
        }
        for (Entry<String, JFieldVar> entry : theClass.fields().entrySet()) {

            String fieldName = entry.getKey();
            if (fieldName.startsWith("_")) {
                if (!JJavaName.isJavaIdentifier(fieldName.substring(1))) {
                    fieldName = fieldName.substring(1); // it was prefixed with '_' so we should use the original name.
                }
            }

            final JType fieldType = entry.getValue().type();
            final Class<?> fieldClass = ReflectUtil.loadClass(fieldType.boxify().erasure().fullName());
            final Class<?> fieldParameterClass;
            if (fieldType instanceof JClass) {
                final JClass fieldClassType = (JClass) fieldType;
                final List<JClass> fieldTypeParameters = fieldClassType.getTypeParameters();
                if (fieldTypeParameters.size() > 1) {
                    throw new FlowableException(
                            String.format("Field type '%s' with more than one parameter is not supported: %S",
                                    fieldClassType, fieldTypeParameters));
                } else if (fieldTypeParameters.isEmpty()) {
                    fieldParameterClass = null;
                } else {
                    final JClass fieldParameterType = fieldTypeParameters.get(0);

                    // Hack because JClass.fullname() doesn't return the right class fullname for a nested class to be
                    // loaded from classloader. It should be contain "$" instead of "." as separator
                    boolean isFieldParameterTypeNeestedClass = false;
                    final Iterator<JDefinedClass> theClassNeestedClassIt = theClass.classes();
                    while (theClassNeestedClassIt.hasNext() && !isFieldParameterTypeNeestedClass) {
                        final JDefinedClass neestedType = theClassNeestedClassIt.next();
                        if (neestedType.name().equals(fieldParameterType.name())) {
                            isFieldParameterTypeNeestedClass = true;
                        }
                    }
                    if (isFieldParameterTypeNeestedClass) {
                        // The parameter type is a nested class
                        fieldParameterClass = ReflectUtil
                                .loadClass(theClass.erasure().fullName() + "$" + fieldParameterType.name());
                    } else {
                        // The parameter type is not a nested class
                        fieldParameterClass = ReflectUtil.loadClass(fieldParameterType.erasure().fullName());
                    }
                }
            } else {
                fieldParameterClass = null;
            }

            structure.setFieldName(index.getAndIncrement(), fieldName, fieldClass, fieldParameterClass);
        }
    }

    protected S2JJAXBModel compileModel(Types types, SchemaCompiler compiler, org.w3c.dom.Element rootTypes) {
        Schema schema = (Schema) types.getExtensibilityElements().get(0);
        compiler.parseSchema(schema.getDocumentBaseURI() + "#types1", rootTypes);
        S2JJAXBModel intermediateModel = compiler.bind();
        return intermediateModel;
    }

    @Override
    public Map<String, StructureDefinition> getStructures() {
        return this.structures;
    }

    @Override
    public Map<String, WSService> getServices() {
        return this.wsServices;
    }

    @Override
    public Map<String, WSOperation> getOperations() {
        return this.wsOperations;
    }
}
